﻿namespace SpikingAnalyticsLib

open System
open System.IO
open System.IO.Compression
open System.Text
open Microsoft.FSharp.Reflection

module IOUtilities =

    // Known tokens
    type Token =
        | OpenParenthesis | CloseParenthesis
        | OpenArray | CloseArray
        | Colon | Semicolon | Comma
        | String of string
        | Number of string
        | Boolean of string
        | Other of string       // e.g. an unquoted string

    let Tokenize source =

        // accumulate a string until the string is terminated by a closing quote
        let rec parseString acc = function
            | '\\' :: '"' :: t -> parseString (acc + "\"") t    // escaped quote
            | '"' :: t -> acc, t                                // closing quote terminates
            | c :: t -> parseString (acc + (c.ToString())) t    // otherwise accumulate
            | _ -> failwith "Malformed string"

        let parseBool first = function
            | 'a' :: 'l' :: 's' :: 'e' :: t when first = 'f' -> "false", t
            | 'r' :: 'u' :: 'e' :: t when first = 't' -> "true", t
            | _ -> failwith "Malformed boolean"

        // accumulate a string token (string or number) until the string is terminated
        // by close parenthesis, colon, comma, whitespace or end of data
        let rec token acc = function
            | (')' :: _) as t -> acc, t                         // closing paren terminates
            | (':' :: _) as t -> acc, t                         // colon terminates
            | (',' :: _) as t -> acc, t                         // comma terminates
            | w :: t when Char.IsWhiteSpace(w) -> acc, t        // whitespace terminates
            | [] -> acc, []                                     // end of list terminates
            | c :: t -> token (acc + (c.ToString())) t          // otherwise accumulate chars
 
        let rec tokenize' acc = function
            | w :: t when Char.IsWhiteSpace(w) -> tokenize' acc t   // skip whitespace
            | '(' :: t -> tokenize' (OpenParenthesis :: acc) t
            | ')' :: t -> tokenize' (CloseParenthesis :: acc) t
            | '[' :: t -> tokenize' (OpenArray :: acc) t
            | ']' :: t -> tokenize' (CloseArray :: acc) t
            | ':' :: t -> tokenize' (Colon :: acc) t
            | ';' :: t -> tokenize' (Semicolon :: acc) t
            | ',' :: t -> tokenize' (Comma :: acc) t
            | '"' :: t ->
                // start of string
                let s, t' = parseString "" t
                tokenize' (Token.String(s) :: acc) t'
            | '-' :: d :: t when Char.IsDigit(d) ->
                // start of negative number
                let n, t' = token ("-" + d.ToString()) t
                tokenize' (Token.Number(n) :: acc) t'
            | '+' :: d :: t | d :: t when Char.IsDigit(d) ->
                // start of positive number
                let n, t' = token (d.ToString()) t
                tokenize' (Token.Number(n) :: acc) t'
            | b :: t when Char.ToLower(b) = 'f' || Char.ToLower(b) = 't' -> // b is one of "FfTt"
                let s, t' = parseBool (Char.ToLower(b)) t
                tokenize' (Token.Boolean(s) :: acc) t'
            | [] -> List.rev acc                                            // end of list terminates
            | u :: t ->                                                     // e.g. an unquoted string
                let s, t' = token (u.ToString()) t
                tokenize' (Token.Other(s) :: acc) t'
            | _ -> failwith "Tokenization error"

        tokenize' [] source

    // Given a Token list, produce a list of objects
    let ConstructObjectList tokenList : Object list=
        let rec ConstructList objAcc tokenList =
            match tokenList with
            | Number(n) :: t ->
                let result =
                    if n.Contains(".") then
                        (box (System.Double.Parse(n))) :: objAcc
                    else
                        try
                            (box (System.Int32.Parse(n))) :: objAcc
                        with
                            // if it overflows then keep it as a string (e.g. might be a long bit string)
                            | :? OverflowException -> (box n) :: objAcc
                ConstructList result t
            | String(s) :: t ->
                let result = (box s) :: objAcc
                ConstructList result t
            | Boolean(b) :: t ->
                let result = (box (System.Boolean.Parse(b))) :: objAcc
                ConstructList result t
            | Other(s) :: t ->
                let result = (box s) :: objAcc          // leave as a string
                ConstructList result t
            | [] ->
                objAcc |> Seq.toList |> List.rev
            | _ :: t -> ConstructList objAcc t

        ConstructList [] tokenList

    // Given a Token list, produce a typed list
    let ConstructList<'T> tokenList : 'T list =        
        let objList = ConstructObjectList tokenList
        objList |> Seq.cast<'T> |> Seq.toList

    // Given a Token list, produce a tuple
    let ConstructTuple<'T> tokenList : 'T =
        let objArray = ConstructObjectList tokenList |> List.toArray
        FSharpValue.MakeTuple(objArray, typeof<'T>) :?> 'T

    // Given a Token list, produce a list of 'T where 'T might be a tuple or other type
    let ConstructTupleList<'T> tokenList : 'T list =
        let rec ConstructTupleList tupleAcc objAcc tokenList =
            match tokenList with
            | OpenParenthesis :: t -> ConstructTupleList tupleAcc [] t
            | Number(n) :: t ->
                let result =
                    if n.Contains(".") then
                        (box (System.Double.Parse(n))) :: objAcc
                    else
                        (box (System.Int32.Parse(n))) :: objAcc
                ConstructTupleList tupleAcc result t
            | String(s) :: t ->
                let result = (box s) :: objAcc
                ConstructTupleList tupleAcc result t
            | Boolean(b) :: t ->
                let result = (box (System.Boolean.Parse(b))) :: objAcc
                ConstructTupleList tupleAcc result t
            | CloseParenthesis :: t ->
                let objArray = objAcc |> List.rev |> List.toArray
                let newTuple = FSharpValue.MakeTuple(objArray, typeof<'T>) :?> 'T
                ConstructTupleList (newTuple :: tupleAcc) objAcc t
            | [] ->
                if tupleAcc.IsEmpty then
                    objAcc |> List.rev |> Seq.cast<'T>
                else
                    tupleAcc |> List.rev |> Seq.cast<'T>
            | _ :: t -> ConstructTupleList tupleAcc objAcc t

        ConstructTupleList [] [] tokenList
        |> Seq.toList

    let GroupTokens groupSize (tokens:seq<Token>) =
        tokens
        |> Seq.mapi (fun index token -> (index / groupSize), token)       // give each token an index that groups groupSize tokens together
        |> Seq.groupBy (fun (index, token) -> index)
        |> Seq.map (fun (key, group) ->
                group
                |> Seq.map (fun (index, token) -> token)
                |> Seq.toList
            )

    /// Parse a string representing a list of some type 'T where 'T might be a tuple or other type
    // For lists of the same type or the same tuple type
    // e.g. ParseStringToList<int * int> "(17, 74) (16, 72) (15, 48) (11, 75) (12, 50) (11, 45) (11, 73) (15, 40) (14, 39)"
    // e.g. ParseStringToList<int * int> "[ (17, 74) (16, 72) (15, 48) (11, 75) (12, 50) (11, 45) (11, 73) (15, 40) (14, 39) ]"
    // e.g. ParseStringToList<int> "1 2 3 4 5 6 7 8 9 0"
    // e.g. ParseStringToList "1 2 3 4 5 6 7 8 9 0"               -> obj list (no type annotation; for non-tuple types only)
    // e.g. ParseStringToList<string> "\"cat\", \"dog\", \"horse\""
    let ParseStringToList<'T> (patternString:string) =
        let tokenList = Tokenize (List.ofArray (patternString.ToCharArray()))
        ConstructTupleList<'T> tokenList

    /// Parse a string representing a sequence of segments where each segment is a list of some type 'T where 'T might be a tuple or other type
    // e.g. ParseStringToSegmentedList<int * double> 2 "3 10.0 5 4.9 8 6.3"     // 3 segments of length 2 where each segment is an int * double
    let ParseStringToSegmentedList<'T> segmentSize (patternString:string) =
        let tokenList = Tokenize (List.ofArray (patternString.ToCharArray()))
        tokenList
        |> GroupTokens segmentSize
        |> Seq.map (fun tokenList -> ConstructTuple<'T> tokenList)
        |> Seq.toList

    /// Parse a string representing some type 'T where 'T might be a tuple or other type
    // For lists of mixed types
    // e.g. IOUtilities.ParseStringToTuple<int * int> "1 2"
    // e.g. IOUtilities.ParseStringToTuple<int * int * int * int * float * float> "27 521 39 761 1.364000 1.000000"
    let ParseStringToTuple<'T> (patternString:string) =
        let tokenList = Tokenize (List.ofArray (patternString.ToCharArray()))
        ConstructTuple<'T> tokenList

    //------------------------------------------------------------------------------------
    // Compression utilities

    let ZipData (data:byte []) =

        let outStream = new MemoryStream()
        use inStream = new MemoryStream(data)
        use gzipStream = new GZipStream(outStream, CompressionMode.Compress, true)

        inStream.CopyTo(gzipStream);
        inStream.Close()
        gzipStream.Close()
        outStream

    let UnzipData (data:byte []) = 

        let outStream = new MemoryStream()
        use inStream = new MemoryStream(data)
        use gzipStream = new GZipStream(inStream, CompressionMode.Decompress, true)

        gzipStream.CopyTo(outStream)
        gzipStream.Close()
        outStream

    // Example usage:
    //let uncompressedLines =
    //    Seq.init 100 (fun index -> index, 100 - index)
    //    |> Seq.map (fun (a, b) -> sprintf "%d\t%d" a b)
    //CompressToFile filePath uncompressedLines
    let CompressToFile filePath (lines:seq<string>) =
        let uncompressed =
            let allLines =
                lines
                |> Seq.fold (fun (sb:StringBuilder) line -> sb.AppendLine(line)) (new StringBuilder())
            Encoding.UTF8.GetBytes(allLines.ToString())
        let data = ZipData uncompressed
        File.WriteAllBytes(filePath, data.ToArray())

    /// Read a compressed text file
    let ReadCompressedFile filePath =
        let unzipped = UnzipData (File.ReadAllBytes(filePath))
        unzipped.Seek(0L, SeekOrigin.Begin) |> ignore
        use sr = new StreamReader(unzipped, Encoding.UTF8)
        seq {
            while (sr.Peek() >= 0) do
                yield sr.ReadLine()
        }
        |> Seq.toList
